﻿using System;
using System.IO;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using System.Xml.Xsl;
using GerbilLib;

namespace GerbilMigrate
{
    /// <summary>
    /// Verifies the given preferences against the xml schema to determine if the
    /// preferences need to be updated for a newer version of the schema. If an update is
    /// needed it is automatically executed through the appropriate xsl transform.
    /// </summary>
    public class PreferencesHelper
    {
        /// <summary>
        /// Default file name for preferences xml file
        /// </summary>
        private static readonly string DefaultFilename = "MediaGerbilPreferences.xml";
        /// <summary>
        /// Temporary file name for xsl transform operations
        /// </summary>
        private static readonly string TempFilename = "MediaGerbilPreferences.temp";
        /// <summary>
        /// List of current xml schemas for differernt Preferences versions ordered from newest to oldest
        /// </summary>
        private static readonly string[] Schemas = { "MediaGerbilPreferences-0.3.0.xsd", "MediaGerbilPreferences-0.2.0.xsd", "MediaGerbilPreferences-0.1.0.xsd" };
        /// <summary>
        /// List of current xsl transforms for different Preferences versions ordered from newest to oldest
        /// </summary>
        private static readonly string[] Transforms = { "MediaGerbilPreferences-0.2.0-0.3.0.xslt", "MediaGerbilPreferences-0.1.0-0.2.0.xslt" };

        /// <summary>
        /// Current schema reader settings
        /// </summary>
        private static XmlReaderSettings schemaReaderSettings = null;
        /// <summary>
        /// Returns the current schema reader settings or creates a new object from default settings
        /// </summary>
        private static XmlReaderSettings SchemaReaderSettings
        {
            get
            {
                if (null == schemaReaderSettings)
                {
                    schemaReaderSettings = new XmlReaderSettings();
                    schemaReaderSettings.ValidationType = ValidationType.Schema;
                    schemaReaderSettings.ValidationFlags = System.Xml.Schema.XmlSchemaValidationFlags.ReportValidationWarnings;
                    schemaReaderSettings.ValidationEventHandler += new System.Xml.Schema.ValidationEventHandler(settings_ValidationEventHandler);
                }

                return schemaReaderSettings;
            }
        }

        /// <summary>
        /// Loads the preferences xml file and validates it against the most recent schema.
        /// If the file does not validate the schema will be upgraded automatically and
        /// then load the Preferences.
        /// </summary>
        /// <returns>Preferences</returns>
        public static Preferences LoadPreferences()
        {
            if (!File.Exists(DefaultFilename))
            {
                Logger.LogMessage("No Preferences found, using default settings.");
                return null;
            }

            int currentSchema = 0;
            bool isValidPreferences = false;
            while (!isValidPreferences && currentSchema < Schemas.Length)
            {
                Logger.LogMessage(string.Format("Trying to validate Preferences against schema {0}!", Schemas[currentSchema]));
                isValidPreferences = ValidatePreferences(currentSchema);
                if (!isValidPreferences) currentSchema++;
            }

            if (currentSchema == 0) return LoadValidatedPreferences();
            Logger.LogMessage("Preferences will be upgraded to latest schema version before loading...");
            return LoadAndMigratePreferences(currentSchema);
        }

        /// <summary>
        /// Validates the Preferences xml file against the given schema.
        /// </summary>
        /// <param name="currentSchema">Index of the schema in the schema list</param>
        /// <returns>True if the Preferences validate against the schema; otherwise false</returns>
        private static bool ValidatePreferences(int currentSchema)
        {
            bool isValidPreferences = true;

            XmlSchemaSet schemaSet = new XmlSchemaSet();
            //schemaSet.Add(null, new XmlTextReader(Schemas[currentSchema]));
            schemaSet.Add(null, new XmlTextReader(XmlLoader.GetEmbeddedXmlStream(Schemas[currentSchema])));
            SchemaReaderSettings.Schemas = schemaSet;
            XmlReader reader = null;
            try
            {
                reader = XmlReader.Create(DefaultFilename, SchemaReaderSettings);
                while (reader.Read()) ;
                Logger.LogMessage(string.Format("Preferences successfully validated against schema {0}!", Schemas[currentSchema]));
            }
            catch (Exception ex)
            {
                Logger.LogMessage(string.Format("Preferences Validation Error: {0}", ex.Message), LoggingLevel.Debug);
                isValidPreferences = false;
            }
            finally
            {
                if (reader != null) reader.Close();
            }

            return isValidPreferences;
        }

        /// <summary>
        /// Loads the Preferences xml file and deserializes it into a Preferences object.
        /// </summary>
        /// <returns>Preferences object</returns>
        private static Preferences LoadValidatedPreferences()
        {
            // TODO: Write upgrade logic to migrate Preferences from previous version
            XmlSerializer serializer = new XmlSerializer(typeof(Preferences));
            serializer.UnknownAttribute += new XmlAttributeEventHandler(serializer_UnknownAttribute);
            serializer.UnknownElement += new XmlElementEventHandler(serializer_UnknownElement);
            serializer.UnknownNode += new XmlNodeEventHandler(serializer_UnknownNode);
            serializer.UnreferencedObject += new UnreferencedObjectEventHandler(serializer_UnreferencedObject);

            Preferences preferences = null;
            try
            {
                XmlReader reader = XmlReader.Create(DefaultFilename);
                preferences = (Preferences)serializer.Deserialize(reader);
                reader.Close();
                Logger.LogMessage("Preferences successfully loaded!");
            }
            catch (FileNotFoundException ex)
            {
                Logger.LogMessage(string.Format("Loading of Preferences failed: {0}", ex.Message));
            }
            return preferences;
        }

        /// <summary>
        /// Event handler for validation events that are fired when validation fails.
        /// </summary>
        /// <param name="sender">Sender object</param>
        /// <param name="e">Validation Event Arguments</param>
        static void settings_ValidationEventHandler(object sender, System.Xml.Schema.ValidationEventArgs e)
        {
            throw new Exception(e.Message, e.Exception);
        }

        /// <summary>
        /// Handles the UnreferencedObject event of the serializer control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.Xml.Serialization.UnreferencedObjectEventArgs"/> instance containing the event data.</param>
        static void serializer_UnreferencedObject(object sender, UnreferencedObjectEventArgs e)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Handles the UnknownNode event of the serializer control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.Xml.Serialization.XmlNodeEventArgs"/> instance containing the event data.</param>
        static void serializer_UnknownNode(object sender, XmlNodeEventArgs e)
        {
            if (e.ObjectBeingDeserialized != null) ((Preferences)e.ObjectBeingDeserialized).DeserializedWithErrors = true;
        }

        /// <summary>
        /// Handles the UnknownElement event of the serializer control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.Xml.Serialization.XmlElementEventArgs"/> instance containing the event data.</param>
        static void serializer_UnknownElement(object sender, XmlElementEventArgs e)
        {
            if (e.ObjectBeingDeserialized != null) ((Preferences)e.ObjectBeingDeserialized).DeserializedWithErrors = true;
        }

        /// <summary>
        /// Handles the UnknownAttribute event of the serializer control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.Xml.Serialization.XmlAttributeEventArgs"/> instance containing the event data.</param>
        static void serializer_UnknownAttribute(object sender, XmlAttributeEventArgs e)
        {
            if (e.ObjectBeingDeserialized != null) ((Preferences)e.ObjectBeingDeserialized).DeserializedWithErrors = true;
        }

        /// <summary>
        /// Saves the preferences and serializes them into an xml file.
        /// </summary>
        /// <param name="preferences">The preferences object to be saved</param>
        public static void SavePreferences(Preferences preferences)
        {
            XmlSerializer serializer = new XmlSerializer(typeof(Preferences));
            TextWriter writer = new StreamWriter(DefaultFilename);
            serializer.Serialize(writer, preferences);
            writer.Close();
            Logger.LogMessage("Preferences successfully saved!");
        }

        /// <summary>
        /// Loads the and migrates the preferences using the given schema.
        /// </summary>
        /// <param name="currentSchema">The current schema index in the schema list.</param>
        /// <returns>Migrated Preferences object</returns>
        private static Preferences LoadAndMigratePreferences(int currentSchema)
        {
            try
            {
                while (currentSchema-- > 0)
                {
                    // Create the XmlReader object.
                    XmlReader reader = XmlReader.Create(DefaultFilename);
                    XslCompiledTransform transform = new XslCompiledTransform();
                    transform.Load(XmlReader.Create(XmlLoader.GetEmbeddedXmlStream(Transforms[currentSchema])));
                    Logger.LogMessage(string.Format("Updating preferences using transform {0}", Transforms[currentSchema]), LoggingLevel.Debug);

                    // Create a writer for writing the transformed file.
                    XmlWriter writer = XmlWriter.Create(TempFilename);
                    // Execute the transformation.
                    transform.Transform(reader, writer);
                    reader.Close();
                    writer.Close();

                    // Shuffle files around
                    File.Move(DefaultFilename, DefaultFilename + ".orig." + currentSchema);
                    File.Move(TempFilename, DefaultFilename);
                }
            }
            catch (Exception ex)
            {
                Logger.LogMessage(string.Format("An error occurred during Preferences Update: {0}", ex.Message), LoggingLevel.Debug);
                return null;
            }

            // Preferences should have been migrated to latest schema version by now
            if (ValidatePreferences(0))
            {
                Preferences p = LoadValidatedPreferences();
                p.UpgradedSchemaVersion = true;
                return p;
            }
            else return null;
        }
    }
}
